import { Inject, Injectable, Optional, Renderer2, SecurityContext } from '@angular/core';
import { CachedIconDefinition, IconDefinition, TwoToneColorPalette } from '../types';
import { isIconDefinition, getNameAndNamespace, getIconDefinitionFromAbbr, warn, getSecondaryColor, withSuffixAndColor, withThemeSuffix, replaceFillColor, hasNamespace } from '../utils';
import { of as rxof, Observable, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { DynamicLoadingTimeoutError, HttpModuleNotImport, IconNotFoundError, SVGTagNotFoundError, UrlNotSafeError } from './icon.error';
import { DomSanitizer } from '@angular/platform-browser';

import { catchError, filter, finalize, map, share, take, tap } from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';

const JSONP_HANDLER_NAME = '__sandmon_icon_load';

@Injectable({
  providedIn: 'root'
})
export class IconService {
  // 是否允许使用jsonp加载
  private enableJsonpLoading: boolean = false;
  private readonly jsonpIconLoad$ = new Subject<IconDefinition>();
  protected httpClient!: HttpClient;
  // 所有图标注册到这里
  private registry = new Map<string, IconDefinition>();
  // 所有缓存过的图标
  private registryCached = new Map<string, CachedIconDefinition>();
  // 保存所有正在请求的图标，保证请求的唯一
  protected inProgressFetches = new Map<string, Observable<IconDefinition | null>>();
  protected assetsUrl: string = '';
  protected twoToneColorPalette: TwoToneColorPalette = {
    primaryColor: '#333333',
    secondaryColor: '#E6E6E6'
  }


  constructor(
    private renderer: Renderer2,
    protected sanitizer: DomSanitizer,
    @Optional() @Inject(DOCUMENT) private document: any
  ) {

  }

  // 添加从icons文件夹静态图标
  public registerIcons(...icons: IconDefinition[]): void {
    icons.forEach((iconItem: IconDefinition) => this.registry.set(iconItem.name, iconItem));
  }

  // 允许使用jsonp
  public useJsonpLoading(): void {
    if (!this.enableJsonpLoading) {
      this.enableJsonpLoading = true;

      window[JSONP_HANDLER_NAME] = (icon: IconDefinition):void => {
        this.jsonpIconLoad$.next(icon)
      }
    } else {
      warn('You are already using jsonp loading.');
    }
  }

  public getRenderedContent(icon: IconDefinition | string, twotoneColor: string): Observable<SVGElement> {
    const definitionOrNull: IconDefinition | null = isIconDefinition(icon) ?
    (icon as IconDefinition) :
    (this.registry.get(icon) || null);

    const $iconDefinition = definitionOrNull ? rxof(definitionOrNull) : this.loadIconDynamically(icon as string);

    return $iconDefinition.pipe(
      map(i => {
        if (!i) {
          throw IconNotFoundError(icon as string);
        }
        return this.loadSVGFromCacheOrCreateNew(i, twotoneColor)
      })
    )
  }

  protected loadIconDynamically(icon: string): Observable<IconDefinition | null> {
    // 如果开发者没有提供HttpClient模块并且不允许使用jsonp, 则抛出错误提示
    if (!this.httpClient && !this.enableJsonpLoading) {
      return rxof(HttpModuleNotImport());
    }
    
    // 如果多个地方用到同一个图标，同时发起请求，则使用inProgressFetches来保存正在请求的所有图标
    let inProgress = this.inProgressFetches.get(icon);
    
    if (!inProgress) {
      const [name, namespace] = getNameAndNamespace(icon);

      const iconDefinition: IconDefinition = namespace ? {name: icon, icon: ''} : getIconDefinitionFromAbbr(name);

      const suffix = this.enableJsonpLoading ? '.js' : '.svg';
      
      const url = (namespace ?
         `${this.assetsUrl}/assets/${namespace}/${name}` :
         `${this.assetsUrl}/assets/${iconDefinition.theme}/${iconDefinition.name}`) + suffix;

         // 防止注入脚本攻击服务器
      const safeUrl = this.sanitizer.sanitize(SecurityContext.URL, url);

      if (!safeUrl) {
        throw UrlNotSafeError(url);
      }

      const source = !this.enableJsonpLoading ?
      this.httpClient.get(safeUrl, { responseType: 'text' }).pipe(map(literal => ({ ...iconDefinition, icon: literal }))) :
      this.loadIconDynamicallyWithJsonp(iconDefinition, safeUrl);

      inProgress = source.pipe(
        tap(definition => this.registerIcons(definition)),
        finalize(() => this.inProgressFetches.delete(icon)),
        catchError(() => rxof(null)),
        share()
      );

      this.inProgressFetches.set(icon, inProgress);
    }
    return inProgress;
  }

  protected loadIconDynamicallyWithJsonp(icon: IconDefinition, url: string): Observable<IconDefinition> {
    return new Observable<IconDefinition>(subscriber => {
      const script = this.document.createElement('script');
      const timer = setTimeout(() => {
        clean()
        subscriber.error(DynamicLoadingTimeoutError());
      }, 6000)

      function clean(): void {
        script.parentNode.removeChild(script);
        clearTimeout(timer)
      }

      script.src = url;
      this.document.body.appendChild(script);
      this.jsonpIconLoad$.pipe(
        filter(i => i.name === icon.name && i.theme === icon.theme),
        take(1)
      ).subscribe(i => {
        subscriber.next(i);
        clean();
      })
    });
  }

  protected loadSVGFromCacheOrCreateNew(icon: IconDefinition, twotoneColor: string): SVGElement {
    let svg: SVGElement;

    const pri = twotoneColor || this.twoToneColorPalette.primaryColor;
    const sec = getSecondaryColor(pri) || this.twoToneColorPalette.secondaryColor;

    const key = icon.theme === 'twotone' ?
    withSuffixAndColor(icon.name, icon.theme, pri, sec) : 
    icon.theme === undefined ?
    icon.name :
    withThemeSuffix(icon.name, icon.theme)

    const cached = this.registryCached.get(key);

    if (cached) {
      svg = cached.icon;
    } else {
      svg = this.setSVGAttribute(
        this.colorizeSVGIcon(
          this.createSVGElementFromString(
            hasNamespace(icon.name) ? icon.icon : replaceFillColor(icon.icon)
          ),
          icon.theme === 'twotone',
          pri,
          sec
        )
      )
      this.registryCached.set(key, {
        ...icon,
        icon: svg
      } as CachedIconDefinition);
    }
    return svg
  }

  protected setSVGAttribute(svg: SVGElement): SVGElement {
    this.renderer.setAttribute(svg, 'width', '1em');
    this.renderer.setAttribute(svg, 'height', '1em');
    return svg;
  }

  protected colorizeSVGIcon(
    svg: SVGElement,
    twotone: boolean,
    pri: string,
    sec: string
  ): SVGElement {
    if (twotone) {
      const children = svg.childNodes;
      const length = children.length;
      for (let i = 0; i < length; i++) {
        const child: HTMLElement = children[i] as HTMLElement;
        if (child.getAttribute('fill') === 'secondaryColor') {
          this.renderer.setAttribute(child, 'fill', sec);
        } else {
          this.renderer.setAttribute(child, 'fill', pri);
        }
      }
    }
    this.renderer.setAttribute(svg, 'fill', 'currentColor');
    return svg;
  }

  protected createSVGElementFromString(svgStr: string): SVGElement {
    const div = this.document.createElement('div');
    div.innerHTML = svgStr;
    const svg: SVGElement = div.querySelector('svg');
    if (!svg) {
      throw SVGTagNotFoundError;
    }
    return svg;
  }
}
